import CanvasContext = WechatMiniprogram.CanvasContext
import Canvas = WechatMiniprogram.Canvas
import Image = WechatMiniprogram.Image
import { Video } from '../Parser/video'
import { Render } from '../Render'
import { Parser } from '../Parser'
import { platformBridge } from '../Platform'
import { getCanvas } from '../Utils'
import { IPlayerOptions, IVideoImage, IContentMode, IDynamicText, IRange, IFillMode } from '../Types'
import { Animator } from './animator'

const wx = platformBridge()

export class Player {
  ctx?: CanvasContext
  canvas?: Canvas
  private elId: string
  private component?: WechatMiniprogram.Component.TrivialInstance
  private url: string
  private loops = 0
  private clearAfterStop = true
  private fillMode: IFillMode = 'Forward'
  private videoItem?: Video
  private contentMode: IContentMode = 'AspectFit'
  private render: Render | undefined
  private animator: Animator = new Animator()
  private currentFrame = 0
  private dynamicImage: { [key: string]: any } = {}
  private dynamicText: { [key: string]: IDynamicText } = {}
  private forwardAnimate = false
  private onFrameFunc?: (frame: number) => void
  private onPercentageFunc?: (percentage: number) => void
  private onFinishedFunc?: () => void
  
  constructor({ id, component, url, fillMode, contentMode, loops, clearAfterStop }: IPlayerOptions) {
    this.elId = id
    this.component = component
    this.url = url
    this.loops = loops ?? 0
    if (typeof clearAfterStop !== 'undefined') this.clearAfterStop = clearAfterStop
    if (fillMode) this.fillMode = fillMode
    if (contentMode) this.contentMode = contentMode
  }
  
  async mount() {
    const parser = new Parser()
    this.videoItem = await parser.init(this.url)
    const { canvas, ctx } = await getCanvas(this.elId, this.component)
    this.canvas = canvas
    this.ctx = ctx
    await this.setVideo()
    this.start()
  }
  
  // 设置数据
  async setVideo() {
    this.currentFrame = 0
    const { videoItem } = this
    if (videoItem) {
      const images = Object.keys(videoItem.props.images).map(async (k: string) => {
        try {
          const imageCtx = await this.parserImage(videoItem.props.images[k])
          return { key: k, value: imageCtx }
        } catch {
          return { key: k, value: undefined }
        }
      })
      const keyedImages = await Promise.all(images)
      const decodedImages: { [key: string]: any } = {}
      keyedImages?.forEach((v: IVideoImage) => {
        decodedImages[v.key] = v.value
      })
      videoItem.decodedImages = decodedImages
      this.render = new Render(this.videoItem!, this.ctx!, this.canvas!)
    } else {
      this.render = undefined
    }
    this.clear()
    this.update()
  }
  
  clear() {
    this.render.clear()
  }
  
  start(reverse: boolean = false) {
    this.stop(false)
    this.startFunc(undefined, reverse, undefined)
  }
  
  startWidthRange(range: IRange, reverse: boolean = false) {
    this.stop(false)
    this.startFunc(range, reverse, undefined)
  }
  
  pause() {
    this.stop(false)
  }
  
  stop(clear?: boolean) {
    this.forwardAnimate = false
    if (this.animator !== undefined) this.animator.stop()
    
    if (clear === undefined) clear = this.clearAfterStop
    
    if (clear) this.clear()
  }
  
  setContentMode(mode: IContentMode) {
    this.contentMode = mode
    this.update()
  }
  
  onFrame(callback: (frame: number) => void) {
    this.onFrameFunc = callback
  }
  
  onFinished(callback: () => void) {
    this.onFinishedFunc = callback
  }
  
  onPercentage(callback: (percentage: number) => void) {
    this.onPercentageFunc = callback
  }
  
  stepToFrame(frame: number, isPlay: boolean = false) {
    const { videoItem } = this
    if (!videoItem || videoItem.frames <= frame || frame < 0) return
    
    this.pause()
    this.currentFrame = frame
    this.update()
    if (isPlay) this.startFunc(undefined, false, this.currentFrame)
  }
  
  stepToPercentage(percentage: number, isPlay: boolean = false) {
    const { videoItem } = this
    if (!videoItem) return
    let frame = percentage * videoItem.frames
    if (frame >= videoItem.frames && frame > 0) frame = videoItem.frames - 1
    this.stepToFrame(frame, isPlay)
  }
  
  setText(dynamicText: IDynamicText, forKey: string) {
    this.dynamicText[forKey] = dynamicText
  }
  
  async setImage(src: Uint8Array | string, forKey: string) {
    this.dynamicImage[forKey] = await this.parserImage(src)
  }
  
  clearDynamic() {
    this.dynamicText = {}
    this.dynamicImage = {}
  }
  
  // 将Uint8Array数据转换为base64图片
  private parserImage(data: Uint8Array | string): Promise<Image> {
    if (!this.canvas) throw 'no canvas'
    
    return new Promise(async (resolve, reject) => {
      try {
        const imageCtx: Image = this.canvas!.createImage()
        const src = typeof data === 'string' ? data : `data:image/png;base64,${wx.arrayBufferToBase64(data as ArrayBuffer)}`
        await new Promise((resolver, rejecter) => {
          imageCtx.onload = resolver
          imageCtx.onerror = rejecter
          imageCtx.src = src
        })
        resolve(imageCtx)
      } catch (err: any) {
        reject(err)
      }
    })
  }
  
  private startFunc(range?: IRange, reverse: boolean = false, fromFrame: number = 0) {
    const { videoItem } = this
    if (!videoItem) return
    
    this.animator = new Animator()
    this.animator.canvas = this.canvas
    
    if (range !== undefined) {
      this.animator.startValue = Math.max(0, range.location)
      this.animator.endValue = Math.min(videoItem.frames - 1, range.location + range.length)
      this.animator.duration = (this.animator.endValue - this.animator.startValue + 1) * (1.0 / videoItem.FPS) * 1000
    } else {
      this.animator.startValue = 0
      this.animator.endValue = videoItem.frames - 1
      this.animator.duration = videoItem.frames * (1.0 / videoItem.FPS) * 1000
    }
    
    this.animator.loops = this.loops <= 0 ? Infinity : this.loops
    this.animator.fillRule = this.fillMode === 'Backward' ? 1 : 0
    this.animator.onUpdate = (value: number) => {
      if (this.currentFrame === Math.floor(value)) return
      
      this.currentFrame = Math.floor(value)
      this.update()
      if (typeof this.onFrameFunc === 'function') this.onFrameFunc(this.currentFrame)
      if (typeof this.onPercentageFunc === 'function') this.onPercentageFunc((this.currentFrame + 1) / videoItem.frames)
    }
    
    this.animator.onEnd = () => {
      this.forwardAnimate = false
      if (this.clearAfterStop) this.clear()
      
      if (typeof this.onFinishedFunc === 'function') this.onFinishedFunc()
    }
    
    if (reverse) {
      this.animator.reverse(fromFrame)
      this.forwardAnimate = false
    } else {
      this.animator.start(fromFrame)
      this.forwardAnimate = true
    }
    
    this.currentFrame = this.animator.startValue
    this.update()
  }
  
  private resize() {
    const { videoItem, contentMode } = this
    if (!this.ctx || !videoItem) return
    
    let scaleX = 1.0
    let scaleY = 1.0
    let translateX = 0.0
    let translateY = 0.0
    const lSize = { width: this.canvas.width, height: this.canvas.height }
    const imageSize = videoItem.videoSize
    
    if (contentMode === 'Fill') {
      scaleX = lSize.width / imageSize.width
      scaleY = lSize.height / imageSize.height
    } else {
      const imageRatio = imageSize.width / imageSize.height
      const viewRatio = lSize.width / lSize.height
      if ((imageRatio >= viewRatio && this.contentMode === 'AspectFit') || (imageRatio <= viewRatio && this.contentMode === 'AspectFill')) {
        scaleX = scaleY = lSize.width / imageSize.width
        translateY = (lSize.height - imageSize.height) / 2.0
      } else if ((imageRatio < viewRatio && this.contentMode === 'AspectFit') || (imageRatio > viewRatio && this.contentMode === 'AspectFill')) {
        scaleX = scaleY = lSize.height / imageSize.height
        translateX = (lSize.width - imageSize.width) / 2.0
      }
    }
    
    if (this.render) {
      this.render.setGlobalTransform({ a: scaleX, b: 0.0, c: 0.0, d: scaleY, tx: translateX, ty: translateY })
    }
  }
  
  private update() {
    this.resize()
    if (!this.render) return
    
    this.render.setDynamicImage(this.dynamicImage)
    this.render.setDynamicText(this.dynamicText)
    this.render.drawFrame(this.currentFrame)
  }
}
